﻿/************************** lib.js *************************************/
// -----------------------------------------------------------------------------
// Native Object extensions

Number.prototype.map = function (istart, istop, ostart, ostop) {
  return ostart + (ostop - ostart) * ((this - istart) / (istop - istart));
};

Number.prototype.limit = function (min, max) {
  return Math.min(max, Math.max(min, this));
};

Number.prototype.round = function (precision) {
  precision = Math.pow(10, precision || 0);
  return Math.round(this * precision) / precision;
};

Number.prototype.floor = function () {
  return Math.floor(this);
};

Number.prototype.ceil = function () {
  return Math.ceil(this);
};

Number.prototype.toInt = function () {
  return (this | 0);
};

Array.prototype.erase = function (item) {
  for (var i = this.length; i--; ) {
    if (this[i] === item) {
      this.splice(i, 1);
    }
  }
  return this;
};

Array.prototype.random = function () {
  return this[Math.floor(Math.random() * this.length)];
};

Function.prototype.bind = function (bind) {
  var self = this;
  return function () {
    var args = Array.prototype.slice.call(arguments);
    return self.apply(bind || null, args);
  };
};

Object.extend = function (destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

function loadScript(url, async) {
  var script = document.createElement('script');
  script.type = 'text/javascript';
  script.src = url;
  script.async = async;
  document.head.appendChild(script);
}

// -----------------------------------------------------------------------------
// Class object based on John Resigs code; inspired by base2 and Prototype
// http://ejohn.org/blog/simple-javascript-inheritance/

var initializing = false, fnTest = /xyz/.test(function () { xyz; }) ? /\bparent\b/ : /.*/;
var Eg = {
  ready: false,
  $new: function (name) {
    return document.createElement(name);
  },
  copy: function (object) {
    if (!object || typeof (object) != 'object' || object instanceof HTMLElement || object instanceof Eg.Class) {
      return object;
    }
    else if (object instanceof Array) {
      var c = [];
      for (var i = 0, l = object.length; i < l; i++) {
        c[i] = Eg.copy(object[i]);
      }
      return c;
    }
    else {
      var c = {};
      for (var i in object) {
        c[i] = Eg.copy(object[i]);
      }
      return c;
    }
  },


  merge: function (original, extended) {
    for (var key in extended) {
      var ext = extended[key];
      if (typeof (ext) != 'object' || ext instanceof HTMLElement || ext instanceof Eg.Class) {
        original[key] = ext;
      }
      else {
        if (!original[key] || typeof (original[key]) != 'object') {
          original[key] = (ext instanceof Array) ? [] : {};
        }
        Eg.merge(original[key], ext);
      }
    }
    return original;
  },

  keysort: function (obj) {
    if (!obj || typeof (obj) != 'object') {
      return [];
    }

    var keys = [], values = [];
    for (var i in obj) {
      keys.push(i);
    }

    keys.sort();
    for (var i = 0; i < keys.length; i++) {
      values.push(obj[keys[i]]);
    }

    return values;
  }
};
Eg.Class = function () { };
var inject = function (prop) {
  var proto = this.prototype;
  var parent = {};
  for (var name in prop) {
    if (typeof (prop[name]) == "function" && typeof (proto[name]) == "function" && fnTest.test(prop[name])) {
      parent[name] = proto[name]; // save original function
      proto[name] = (function (name, fn) {
        return function () {
          var tmp = this.parent;
          this.parent = parent[name];
          var ret = fn.apply(this, arguments);
          this.parent = tmp;
          return ret;
        };
      })(name, prop[name]);
    }
    else {
      proto[name] = prop[name];
    }
  }
};

Eg.Class.extend = function (prop) {
  var parent = this.prototype;

  initializing = true;
  var prototype = new this();
  initializing = false;

  for (var name in prop) {
    if (typeof (prop[name]) == "function" && typeof (parent[name]) == "function" && fnTest.test(prop[name])) {
      prototype[name] = (function (name, fn) {
        return function () {
          var tmp = this.parent;
          this.parent = parent[name];
          var ret = fn.apply(this, arguments);
          this.parent = tmp;
          return ret;
        };
      })(name, prop[name]);
    }
    else {
      prototype[name] = prop[name];
    }
  }

  function Class() {
    if (!initializing) {

      // If this class has a staticInstantiate method, invoke it
      // and check if we got something back. If not, the normal
      // constructor (init) is called.
      if (this.staticInstantiate) {
        var obj = this.staticInstantiate.apply(this, arguments);
        if (obj) {
          return obj;
        }
      }
      for (var p in this) {
        if (typeof (this[p]) == 'object') {
          this[p] = Eg.copy(this[p]); // deep copy!
        }
      }
      if (this.init) {
        this.init.apply(this, arguments);
      }
    }
    return this;
  }

  Class.prototype = prototype;
  Class.constructor = Class;
  Class.extend = arguments.callee;
  Class.inject = inject;

  return Class;
};


/*********************************** engine.js ****************************/
/*
**  引擎
*/
Object.extend(Eg, {
  ready: false,
  resources: [],
  init: function (opts) {
    Eg.sys = new Eg.Sys(opts);
    Eg.input = new Eg.Input();
    Eg.soundManager = new Eg.SoundManager();
    Eg.music = new Eg.Music();
    Eg.loader = new Eg.Loader(opts);
    Eg.loader.addRes(new Eg.Font(opts.font));

    Eg.ready = true;
    Eg.loader.load();
  },

  addResource: function (resource) {
    Eg.resources.push(resource);
  },


  setNocache: function (set) {
    Eg.nocache = set ? '?' + Date.now() : '';
  },


  // Stubs for Eg.Debug
  log: function () { },
  show: function (name, number) { },
  mark: function (msg, color) { }
});

/*********************************** Timer.js ****************************/
Eg.Timer = Eg.Class.extend({
  target: 0,
  base: 0,
  last: 0,

  init: function (seconds) {
    this.base = Eg.Timer.time;
    this.last = Eg.Timer.time;

    this.target = seconds || 0;
  },


  set: function (seconds) {
    this.target = seconds || 0;
    this.base = Eg.Timer.time;
  },


  reset: function () {
    this.base = Eg.Timer.time;
  },


  tick: function () {
    var delta = Eg.Timer.time - this.last;
    this.last = Eg.Timer.time;
    return delta;
  },


  delta: function () {
    return Eg.Timer.time - this.base - this.target;
  }
});

Eg.Timer._last = 0;
Eg.Timer.time = 0;
Eg.Timer.timeScale = 1;
Eg.Timer.maxStep = 0.05;

Eg.Timer.step = function () {
  var current = Date.now();
  var delta = (current - Eg.Timer._last) / 1000;
  Eg.Timer.time += Math.min(delta, Eg.Timer.maxStep) * Eg.Timer.timeScale;
  Eg.Timer._last = current;
};


/*********************************** Sys.js ****************************/
Eg.Sys = Eg.Class.extend({
  fps: 30,
  width: 320,
  height: 240,
  realWidth: 320,
  realHeight: 240,
  scale: 1,
  autoStart: true,

  tick: 0,
  smoothPositioning: true,

  clock: null,

  init: function (opts) {
    this.fps = opts.fps;
    this.clock = new Eg.Timer();
    this.canvas = document.getElementById(opts.canvasId);
    this.width = opts.width;
    this.height = opts.height;
    this.autoStart = opts.AutoStart;
    this.scale = opts.scale;
    this.resize(this.width, this.height, this.scale);
    this.context = this.canvas.getContext('2d');
  },
  resize: function (width, height, scale) {
    this.width = width;
    this.height = height;
    this.scale = scale || this.scale;

    this.realWidth = this.width * this.scale;
    this.realHeight = this.height * this.scale;
    this.canvas.width = this.realWidth;
    this.canvas.height = this.realHeight;
  },

  setGame: function (gameClass) {
    if (this.running) {
      this.newGameClass = gameClass;
    }
    else {
      this.setGameNow(gameClass);
    }
  },


  setGameNow: function (gameClass) {
    Eg.game = new (gameClass)();
    Eg.sys.setDelegate(Eg.game);
  },


  setDelegate: function (object) {
    if (typeof (object.run) == 'function') {
      this.delegate = object;
      if (this.autoStart) {
        this.startRunLoop();
      }
    } else {
      throw ('System.setDelegate: No run() function in object');
    }
  },


  stopRunLoop: function () {
    clearInterval(this.intervalId);
    this.running = false;
  },


  startRunLoop: function () {
    this.stopRunLoop();
    this.intervalId = setInterval(this.run.bind(this), 1000 / this.fps);
    this.running = true;
  },

  clear: function (color) {
    this.context.fillStyle = color;
    this.context.fillRect(0, 0, this.realWidth, this.realHeight);
  },
  run: function () {
    Eg.Timer.step();
    this.tick = this.clock.tick();

    this.delegate.run();
    Eg.input.clearPressed();

    if (this.newGameClass) {
      this.setGameNow(this.newGameClass);
      this.newGameClass = null;
    }
  },
  getDrawPos: function (p) {
    return this.smoothPositioning ? Math.round(p * this.scale) : Math.round(p) * this.scale;
  }
});


/*********************************** Image.js ****************************/
Eg.Image = Eg.Class.extend({
  data: null,
  width: 0,
  height: 0,
  loaded: false,
  failed: false,
  path: '',
  loadCallback: null,
  init: function (path) {
    this.path = path;
  },
  load: function (loadCallback) {
    if (this.loaded) {
      if (loadCallback) {
        loadCallback(this.path, true);
      }
      return;
    }
    else if (!this.loaded && Eg.ready) {
      this.loadCallback = loadCallback || null;
      this.data = new Image();
      this.data.onload = this.onload.bind(this);
      this.data.onerror = this.onerror.bind(this);
      this.data.src = this.path;
    }
    else {
      Eg.addResource(this);
    }
    Eg.Image.cache[this.path] = this;
  },

  reload: function () {
    this.loaded = false;
    this.data = new Image();
    this.data.onload = this.onload.bind(this);
    this.data.src = this.path + '?' + Date.now();
  },


  onload: function (event) {
    this.width = this.data.width;
    this.height = this.data.height;
    if (Eg.sys.scale != 1) {
      this.resize(Eg.sys.scale);
    }
    this.loaded = true;
    if (this.loadCallback) {
      this.loadCallback(this.path, true);
    }
  },


  onerror: function (event) {
    this.failed = true;

    if (this.loadCallback) {
      this.loadCallback(this.path, false);
    }
  },

  resize: function (scale) {
    // Nearest-Neighbor scaling

    // The original image is drawn into an offscreen canvas of the same size
    // and copied into another offscreen canvas with the new size. 
    // The scaled offscreen canvas becomes the image (data) of this object.

    var widthScaled = this.width * scale;
    var heightScaled = this.height * scale;

    var orig = Eg.$new('canvas');
    orig.width = this.width;
    orig.height = this.height;
    var origCtx = orig.getContext('2d');
    origCtx.drawImage(this.data, 0, 0, this.width, this.height, 0, 0, this.width, this.height);
    var origPixels = origCtx.getImageData(0, 0, this.width, this.height);

    var scaled = Eg.$new('canvas');
    scaled.width = widthScaled;
    scaled.height = heightScaled;
    var scaledCtx = scaled.getContext('2d');
    var scaledPixels = scaledCtx.getImageData(0, 0, widthScaled, heightScaled);

    for (var y = 0; y < heightScaled; y++) {
      for (var x = 0; x < widthScaled; x++) {
        var index = (Math.floor(y / scale) * this.width + Math.floor(x / scale)) * 4;
        var indexScaled = (y * widthScaled + x) * 4;
        scaledPixels.data[indexScaled] = origPixels.data[index];
        scaledPixels.data[indexScaled + 1] = origPixels.data[index + 1];
        scaledPixels.data[indexScaled + 2] = origPixels.data[index + 2];
        scaledPixels.data[indexScaled + 3] = origPixels.data[index + 3];
      }
    }
    scaledCtx.putImageData(scaledPixels, 0, 0);
    this.data = scaled;
  },


  draw: function (targetX, targetY, sourceX, sourceY, width, height) {
    if (!this.loaded) { return; }

    var scale = Eg.sys.scale;
    sourceX = sourceX ? sourceX * scale : 0;
    sourceY = sourceY ? sourceY * scale : 0;
    width = (width ? width : this.width) * scale;
    height = (height ? height : this.height) * scale;

    Eg.sys.context.drawImage(
			this.data, sourceX, sourceY, width, height,
			Eg.sys.getDrawPos(targetX),
			Eg.sys.getDrawPos(targetY),
			width, height
		);

    Eg.Image.drawCount++;
  },


  drawTile: function (targetX, targetY, tile, tileWidth, tileHeight, flipX, flipY) {
    tileHeight = tileHeight ? tileHeight : tileWidth;

    if (!this.loaded || tileWidth > this.width || tileHeight > this.height) { return; }

    var scale = Eg.sys.scale;
    var tileWidthScaled = tileWidth * scale;
    var tileHeightScaled = tileHeight * scale;

    var scaleX = flipX ? -1 : 1;
    var scaleY = flipY ? -1 : 1;

    if (flipX || flipY) {
      Eg.sys.context.save();
      Eg.sys.context.scale(scaleX, scaleY);
    }
    Eg.sys.context.drawImage(
			this.data,
			(Math.floor(tile * tileWidth) % this.width) * scale,
			(Math.floor(tile * tileWidth / this.width) * tileHeight) * scale,
			tileWidthScaled,
			tileHeightScaled,
			Eg.sys.getDrawPos(targetX) * scaleX - (flipX ? tileWidthScaled : 0),
			Eg.sys.getDrawPos(targetY) * scaleY - (flipY ? tileHeightScaled : 0),
			tileWidthScaled,
			tileHeightScaled
		);
    if (flipX || flipY) {
      Eg.sys.context.restore();
    }

    Eg.Image.drawCount++;
  }
});
Eg.Image.drawCount = 0;
Eg.Image.cache = {};
Eg.Image.reloadCache = function () {
  for (path in Eg.Image.cache) {
    Eg.Image.cache[path].reload();
  }
};


/*********************************** Font.js ****************************/
Eg.Font = Eg.Image.extend({
  widthMap: [],
  indices: [],
  firstChar: 32,
  height: 0,
  onload: function (ev) {
    this._loadMetrics(this.data);
    this.parent(ev);
  },

  widthForString: function (s) {
    var width = 0;
    for (var i = 0; i < s.length; i++) {
      width += this.widthMap[s.charCodeAt(i) - this.firstChar] + 1;
    }
    return width;
  },


  draw: function (text, x, y, align) {
    if (typeof (text) != 'string') {
      text = text.toString();
    }

    if (align == Eg.Font.ALIGN.RIGHT || align == Eg.Font.ALIGN.CENTER) {
      var width = 0;
      for (var i = 0; i < text.length; i++) {
        var c = text.charCodeAt(i);
        width += this.widthMap[c - this.firstChar] + 1;
      }
      x -= align == Eg.Font.ALIGN.CENTER ? width / 2 : width;
    }


    for (var i = 0; i < text.length; i++) {
      var c = text.charCodeAt(i);
      x += this._drawChar(c - this.firstChar, x, y);
    }
    Eg.Image.drawCount += text.length;
  },


  _drawChar: function (c, targetX, targetY) {
    if (!this.loaded || c < 0 || c >= this.indices.length) { return 0; }

    var scale = Eg.sys.scale;


    var charX = this.indices[c] * scale;
    var charY = 0;
    var charWidth = this.widthMap[c] * scale;
    var charHeight = (this.height - 2) * scale;

    Eg.sys.context.drawImage(
			this.data,
			charX, charY,
			charWidth, charHeight,
			Eg.sys.getDrawPos(targetX), Eg.sys.getDrawPos(targetY),
			charWidth, charHeight
		);

    return this.widthMap[c] + 1;
  },


  _loadMetrics: function (image) {
    // Draw the bottommost line of this font image into an offscreen canvas
    // and analyze it pixel by pixel.
    // A run of non-transparent pixels represents a character and its width

    this.height = image.height - 1;
    this.widthMap = [];
    this.indices = [];

    var canvas = Eg.$new('canvas');
    canvas.width = image.width;
    canvas.height = image.height;
    var ctx = canvas.getContext('2d');
    ctx.drawImage(image, 0, 0);
    var px = ctx.getImageData(0, image.height - 1, image.width, 1);

    var currentChar = 0;
    var currentWidth = 0;
    for (var x = 0; x < image.width; x++) {
      var index = x * 4 + 3; // alpha component of this pixel
      if (px.data[index] != 0) {
        currentWidth++;
      }
      else if (px.data[index] == 0 && currentWidth) {
        this.widthMap.push(currentWidth);
        this.indices.push(x - currentWidth);
        currentChar++;
        currentWidth = 0;
      }
    }
    this.widthMap.push(currentWidth);
    this.indices.push(x - currentWidth);
  }
});

Eg.Font.ALIGN = {
  LEFT: 0,
  RIGHT: 1,
  CENTER: 2
};


/*********************************** Input.js ****************************/
Eg.KEY = {
  'MOUSE1': -1,
  'MOUSE2': -3,
  'MWHEEL_UP': -4,
  'MWHEEL_DOWN': -5,

  'BACKSPACE': 8,
  'TAB': 9,
  'ENTER': 13,
  'PAUSE': 19,
  'CAPS': 20,
  'ESC': 27,
  'SPACE': 32,
  'PAGE_UP': 33,
  'PAGE_DOWN': 34,
  'END': 35,
  'HOME': 36,
  'LEFT_ARROW': 37,
  'UP_ARROW': 38,
  'RIGHT_ARROW': 39,
  'DOWN_ARROW': 40,
  'INSERT': 45,
  'DELETE': 46,
  '_0': 48,
  '_1': 49,
  '_2': 50,
  '_3': 51,
  '_4': 52,
  '_5': 53,
  '_6': 54,
  '_7': 55,
  '_8': 56,
  '_9': 57,
  'A': 65,
  'B': 66,
  'C': 67,
  'D': 68,
  'E': 69,
  'F': 70,
  'G': 71,
  'H': 72,
  'I': 73,
  'J': 74,
  'K': 75,
  'L': 76,
  'M': 77,
  'N': 78,
  'O': 79,
  'P': 80,
  'Q': 81,
  'R': 82,
  'S': 83,
  'T': 84,
  'U': 85,
  'V': 86,
  'W': 87,
  'X': 88,
  'Y': 89,
  'Z': 90,
  'NUMPAD_0': 96,
  'NUMPAD_1': 97,
  'NUMPAD_2': 98,
  'NUMPAD_3': 99,
  'NUMPAD_4': 100,
  'NUMPAD_5': 101,
  'NUMPAD_6': 102,
  'NUMPAD_7': 103,
  'NUMPAD_8': 104,
  'NUMPAD_9': 105,
  'MULTIPLY': 106,
  'ADD': 107,
  'SUBSTRACT': 109,
  'DECIMAL': 110,
  'DIVIDE': 111,
  'F1': 112,
  'F2': 113,
  'F3': 114,
  'F4': 115,
  'F5': 116,
  'F6': 117,
  'F7': 118,
  'F8': 119,
  'F9': 120,
  'F10': 121,
  'F11': 122,
  'F12': 123,
  'SHIFT': 16,
  'CTRL': 17,
  'ALT': 18,
  'PLUS': 187,
  'COMMA': 188,
  'MINUS': 189,
  'PERIOD': 190
};


Eg.Input = Eg.Class.extend({
  bindings: {},
  actions: {},
  presses: {},
  locks: {},
  delayedKeyup: [],

  isUsingMouse: false,
  isUsingKeyboard: false,
  isUsingAccelerometer: false,
  mouse: { x: 0, y: 0 },
  accel: { x: 0, y: 0, z: 0 },
  useraction: null,


  initMouse: function () {
    if (this.isUsingMouse) { return; }
    this.isUsingMouse = true;
    window.addEventListener('mousewheel', this.mousewheel.bind(this), false);
    Eg.system.canvas.addEventListener('contextmenu', this.contextmenu.bind(this), false);
    Eg.system.canvas.addEventListener('mousedown', this.keydown.bind(this), false);
    Eg.system.canvas.addEventListener('mouseup', this.keyup.bind(this), false);
    Eg.system.canvas.addEventListener('mousemove', this.mousemove.bind(this), false);

    Eg.system.canvas.addEventListener('touchstart', this.keydown.bind(this), false);
    Eg.system.canvas.addEventListener('touchend', this.keyup.bind(this), false);
    Eg.system.canvas.addEventListener('touchmove', this.mousemove.bind(this), false);
  },


  initKeyboard: function () {
    if (this.isUsingKeyboard) { return; }
    this.isUsingKeyboard = true;
    window.addEventListener('keydown', this.keydown.bind(this), false);
    window.addEventListener('keyup', this.keyup.bind(this), false);
  },


  initAccelerometer: function () {
    if (this.isUsingAccelerometer) { return; }
    window.addEventListener('devicemotion', this.devicemotion.bind(this), false);
  },


  mousewheel: function (event) {
    var code = event.wheel > 0 ? Eg.KEY.MWHEEL_UP : Eg.KEY.MWHEEL_DOWN;
    var action = this.bindings[code];
    if (action) {
      this.actions[action] = true;
      this.presses[action] = true;
      event.stopPropagation();
      this.delayedKeyup.push(action);
    }
  },


  mousemove: function (event) {
    var el = Eg.system.canvas;
    var pos = { left: 0, top: 0 };
    while (el != null) {
      pos.left += el.offsetLeft;
      pos.top += el.offsetTop;
      el = el.offsetParent;
    }
    var tx = event.pageX;
    var ty = event.pageY;
    if (event.touches) {
      tx = event.touches[0].clientX;
      ty = event.touches[0].clientY;
    }

    this.mouse.x = (tx - pos.left) / Eg.system.scale;
    this.mouse.y = (ty - pos.top) / Eg.system.scale;
  },


  contextmenu: function (event) {
    if (this.bindings[Eg.KEY.MOUSE2]) {
      event.stopPropagation();
      event.preventDefault();
    }
  },


  keydown: function (event) {
    if (event.target.type == 'text') { return; }

    var code = event.type == 'keydown'
			? event.keyCode
			: (event.button == 2 ? Eg.KEY.MOUSE2 : Eg.KEY.MOUSE1);

    if (event.type == 'touchstart' || event.type == 'mousedown') {
      this.mousemove(event);
    }

    if (this.useraction && typeof this.useraction == 'function') {
      this.useraction(event);
    }

    var action = this.bindings[code];
    if (action) {
      this.actions[action] = true;
      if (!this.locks[action]) {
        this.presses[action] = true;
        this.locks[action] = true;
      }
      event.stopPropagation();
      event.preventDefault();
    }
  },


  keyup: function (event) {
    if (event.target.type == 'text') { return; }

    var code = event.type == 'keyup'
			? event.keyCode
			: (event.button == 2 ? Eg.KEY.MOUSE2 : Eg.KEY.MOUSE1);

    if (this.useraction && typeof this.useraction == 'function') {
      this.useraction(event);
    }

    var action = this.bindings[code];
    if (action) {
      this.delayedKeyup.push(action);
      event.stopPropagation();
      event.preventDefault();
    }
  },


  devicemotion: function (event) {
    this.accel = event.accelerationIncludingGravity;
  },


  bind: function (key, action) {
    if (key < 0) { this.initMouse(); }
    else if (key > 0) { this.initKeyboard(); }
    this.bindings[key] = action;
  },


  bindTouch: function (selector, action) {
    var element = Eg.$(selector);

    var that = this;
    element.addEventListener('touchstart', function (ev) {
      that.touchStart(ev, action);
    }, false);

    element.addEventListener('touchend', function (ev) {
      that.touchEnd(ev, action);
    }, false);
  },


  unbind: function (key) {
    this.bindings[key] = null;
  },


  unbindAll: function () {
    this.bindings = [];
  },


  state: function (action) {
    return this.actions[action];
  },


  pressed: function (action) {
    return this.presses[action];
  },


  clearPressed: function () {
    for (var i = 0; i < this.delayedKeyup.length; i++) {
      var action = this.delayedKeyup[i];
      this.actions[action] = false;
      this.locks[action] = false;
    }
    this.delayedKeyup = [];
    this.presses = {};
  },

  touchStart: function (event, action) {
    this.actions[action] = true;
    this.presses[action] = true;

    event.stopPropagation();
    event.preventDefault();
    return false;
  },


  touchEnd: function (event, action) {
    this.delayedKeyup.push(action);
    event.stopPropagation();
    event.preventDefault();
    return false;
  }
});


/*********************************** Map.js ****************************/
Eg.Map = Eg.Class.extend({
  tilesize: 8,
  width: 1,
  height: 1,
  data: [[]],


  init: function (tilesize, data) {
    this.tilesize = tilesize;
    this.data = data;
    this.height = data.length;
    this.width = data[0].length;
  },


  getTile: function (x, y) {
    var tx = Math.floor(x / this.tilesize);
    var ty = Math.floor(y / this.tilesize);
    if (
			(tx >= 0 && tx < this.width) &&
			(ty >= 0 && ty < this.height)
		) {
      return this.data[ty][tx];
    }
    else {
      return 0;
    }
  },


  setTile: function (x, y, tile) {
    var tx = Math.floor(x / this.tilesize);
    var ty = Math.floor(y / this.tilesize);
    if (
			(tx >= 0 && tx < this.width) &&
			(ty >= 0 && ty < this.height)
		) {
      this.data[ty][tx] = tile;
    }
  }
});


/*********************************** Camera.js ****************************/
Eg.Camera = Eg.Class.extend({
  trap: {
    pos: {
      x: 0,
      y: 0
    },
    size: {
      x: 16,
      y: 16
    }
  },
  max: {
    x: 0,
    y: 0
  },
  offset: {
    x: 0,
    y: 0
  },
  pos: {
    x: 0,
    y: 0
  },
  damping: 5,
  lookAhead: {
    x: 0,
    y: 0
  },
  currentLookAhead: {
    x: 0,
    y: 0
  },
  debug: false,
  init: function (offsetX, offsetY, damping) {
    this.offset.x = offsetX;
    this.offset.y = offsetY;
    this.damping = damping;
  },
  set: function (entity) {
    this.pos.x = entity.pos.x - this.offset.x;
    this.pos.y = entity.pos.y - this.offset.y;
    this.trap.pos.x = entity.pos.x - this.trap.size.x / 2;
    this.trap.pos.y = entity.pos.y - this.trap.size.y;
  },
  follow: function (entity) {
    this.pos.x = this.move('x', entity.pos.x, entity.size.x);
    this.pos.y = this.move('y', entity.pos.y, entity.size.y);
    Eg.game.screen.x = this.pos.x;
    Eg.game.screen.y = this.pos.y;
  },
  move: function (axis, pos, size) {
    var lookAhead = 0;
    if (pos < this.trap.pos[axis]) {
      this.trap.pos[axis] = pos;
      this.currentLookAhead[axis] = this.lookAhead[axis];
    } else if (pos + size > this.trap.pos[axis] + this.trap.size[axis]) {
      this.trap.pos[axis] = pos + size - this.trap.size[axis];
      this.currentLookAhead[axis] = -this.lookAhead[axis];
    }
    return (this.pos[axis] - (this.pos[axis] - this.trap.pos[axis] + this.offset[axis] + this.currentLookAhead[axis]) * Eg.sys.tick * this.damping).limit(0, this.max[axis]);
  },
  draw: function () {
    if (this.debug) {
      Eg.sys.context.fillStyle = 'rgba(255,0,255,0.3)';
      Eg.sys.context.fillRect((this.trap.pos.x - this.pos.x) * Eg.sys.scale, (this.trap.pos.y - this.pos.y) * Eg.sys.scale, this.trap.size.x * Eg.sys.scale, this.trap.size.y * Eg.sys.scale);
    }
  }
});


/*********************************** Animation.js ****************************/

Eg.AnimationSheet = Eg.Class.extend({
  width: 8,
  height: 8,
  image: null,

  init: function (path, width, height) {
    this.width = width;
    this.height = height;

    this.image = new Eg.Image(path);
    this.image.load();
  }
});



Eg.Animation = Eg.Class.extend({
  sheet: null,
  timer: null,

  sequence: [],
  flip: { x: false, y: false },
  pivot: { x: 0, y: 0 },

  frame: 0,
  tile: 0,
  loopCount: 0,
  alpha: 1,
  angle: 0,


  init: function (sheet, frameTime, sequence, stop) {
    this.sheet = sheet;
    this.pivot = { x: sheet.width / 2, y: sheet.height / 2 };
    this.timer = new Eg.Timer();

    this.frameTime = frameTime;
    this.sequence = sequence;
    this.stop = !!stop;
    this.tile = this.sequence[0];
  },


  rewind: function () {
    this.timer.reset();
    this.loopCount = 0;
    this.tile = this.sequence[0];
    return this;
  },


  gotoFrame: function (f) {
    this.timer.set(this.frameTime * -f);
    this.update();
  },


  gotoRandomFrame: function () {
    this.gotoFrame(Math.floor(Math.random() * this.sequence.length))
  },


  update: function () {
    var frameTotal = Math.floor(this.timer.delta() / this.frameTime);
    this.loopCount = Math.floor(frameTotal / this.sequence.length);
    if (this.stop && this.loopCount > 0) {
      this.frame = this.sequence.length - 1;
    }
    else {
      this.frame = frameTotal % this.sequence.length;
    }
    this.tile = this.sequence[this.frame];
  },


  draw: function (targetX, targetY) {
    var bbsize = Math.max(this.sheet.width, this.sheet.height);

    // On screen?
    if (
		   targetX > Eg.sys.width || targetY > Eg.sys.height ||
		   targetX + bbsize < 0 || targetY + bbsize < 0
		) {
      return;
    }

    if (this.alpha != 1) {
      Eg.sys.context.globalAlpha = this.alpha;
    }

    if (this.angle == 0) {
      this.sheet.image.drawTile(
				targetX, targetY,
				this.tile, this.sheet.width, this.sheet.height,
				this.flip.x, this.flip.y
			);
    }
    else {
      Eg.sys.context.save();
      Eg.sys.context.translate(
				Eg.sys.getDrawPos(targetX + this.pivot.x),
				Eg.sys.getDrawPos(targetY + this.pivot.y)
			);
      Eg.sys.context.rotate(this.angle);
      this.sheet.image.drawTile(
				-this.pivot.x, -this.pivot.y,
				this.tile, this.sheet.width, this.sheet.height,
				this.flip.x, this.flip.y
			);
      Eg.sys.context.restore();
    }

    if (this.alpha != 1) {
      Eg.sys.context.globalAlpha = 1;
    }
  }
});


/*********************************** Sound.js ****************************/
Eg.SoundManager = Eg.Class.extend({
  clips: {},
  volume: 1,
  format: null,

  init: function () {
    // Probe sound formats and determine the file extension to load
    var probe = new Audio();
    for (var i = 0; i < Eg.Sound.use.length; i++) {
      var format = Eg.Sound.use[i];
      if (probe.canPlayType(format.mime)) {
        this.format = format;
        break;
      }
    }

    // No compatible format found? -> Disable sound
    if (!this.format) {
      Eg.Sound.enabled = false;
    }
  },


  load: function (path, multiChannel, loadCallback) {

    // Path to the soundfile with the right extension (.ogg or .mp3)
    var realPath = path.match(/^(.*)\.[^\.]+$/)[1] + '.' + this.format.ext;

    // Sound file already loaded?
    if (this.clips[path]) {

      // Only loaded as single channel and now requested as multichannel?
      if (multiChannel && this.clips[path].length < Eg.Sound.channels) {
        for (var i = this.clips[path].length; i < Eg.Sound.channels; i++) {
          var a = new Audio(realPath);
          a.load();
          this.clips[path].push(a);
        }
      }
      return this.clips[path][0];
    }

    var clip = new Audio(realPath);
    if (loadCallback) {

      // The canplaythrough event is dispatched when the browser determines
      // that the sound can be played without interuption, provided the
      // download rate doesn't change.
      // FIXME: Mobile Safari doesn't seem to dispatch this event at all?
      clip.addEventListener('canplaythrough', function (ev) {
        this.removeEventListener('canplaythrough', arguments.callee, false);
        loadCallback(path, true, ev);
      }, false);

      // FIXME: Sometimes Firefox aborts loading sounds for no reason(?),
      // tell the callback that everything went fine anyway
      // Haven't been able to determine when or why this happens :/
      // Update: Firefox4 doesn't have this problem anymore, but
      // now IE9 does :(
      clip.addEventListener('error', function (ev) {
        loadCallback(path, true, ev); // should pass 'false'
      }, false);
    }
    clip.load();

    this.clips[path] = [clip];
    if (multiChannel) {
      for (var i = 1; i < Eg.Sound.channels; i++) {
        var a = new Audio(realPath);
        a.load();
        this.clips[path].push(a);
      }
    }

    return clip;
  },


  get: function (path) {
    // Find and return a channel that is not currently playing	
    var channels = this.clips[path];
    for (var i = 0, clip; clip = channels[i++]; ) {
      if (clip.paused || clip.ended) {
        if (clip.ended) {
          clip.currentTime = 0;
        }
        return clip;
      }
    }

    // Still here? Pause and rewind the first channel
    channels[0].pause();
    channels[0].currentTime = 0;
    return channels[0];
  }
});



Eg.Music = Eg.Class.extend({
  tracks: [],
  currentTrack: null,
  currentIndex: 0,
  random: false,

  _volume: 1,
  _loop: false,
  _fadeInterval: 0,
  _fadeTimer: null,
  _endedCallbackBound: null,


  init: function () {
    this._endedCallbackBound = this._endedCallback.bind(this);

    if (Object.defineProperty) { // Standard
      Object.defineProperty(this, "volume", {
        get: this.getVolume.bind(this),
        set: this.setVolume.bind(this)
      });

      Object.defineProperty(this, "loop", {
        get: this.getLooping.bind(this),
        set: this.setLooping.bind(this)
      });
    }
    else if (this.__defineGetter__) { // Non-standard
      this.__defineGetter__('volume', this.getVolume.bind(this));
      this.__defineSetter__('volume', this.setVolume.bind(this));

      this.__defineGetter__('loop', this.getLooping.bind(this));
      this.__defineSetter__('loop', this.setLooping.bind(this));
    }
  },


  add: function (music) {
    if (!Eg.Sound.enabled) {
      return;
    }

    var path = music instanceof Eg.Sound ? music.path : music;

    var track = Eg.soundManager.load(path, false);
    track.loop = this._loop;
    track.volume = this._volume;
    track.addEventListener('ended', this._endedCallbackBound, false);
    this.tracks.push(track);

    if (!this.currentTrack) {
      this.currentTrack = track;
    }
  },


  next: function () {
    if (!this.tracks.length) { return; }

    this.stop();
    this.currentIndex = this.random
			? Math.floor(Math.random() * this.tracks.length)
			: (this.currentIndex + 1) % this.tracks.length;
    this.currentTrack = this.tracks[this.currentIndex];
    this.play();
  },


  pause: function () {
    if (!this.currentTrack) { return; }
    this.currentTrack.pause();
  },


  stop: function () {
    if (!this.currentTrack) { return; }
    this.currentTrack.pause();
    this.currentTrack.currentTime = 0;
  },


  play: function () {
    if (!this.currentTrack) { return; }
    this.currentTrack.play();
  },


  getLooping: function () {
    return this._loop;
  },


  setLooping: function (l) {
    this._loop = l;
    for (var i in this.tracks) {
      this.tracks[i].loop = l;
    }
  },


  getVolume: function () {
    return this._volume;
  },


  setVolume: function (v) {
    this._volume = v.limit(0, 1);
    for (var i in this.tracks) {
      this.tracks[i].volume = this._volume;
    }
  },


  fadeOut: function (time) {
    if (!this.currentTrack) { return; }

    clearInterval(this._fadeInterval);
    this.fadeTimer = new Eg.Timer(time);
    this._fadeInterval = setInterval(this._fadeStep.bind(this), 50);
  },


  _fadeStep: function () {
    var v = this.fadeTimer.delta()
			.map(-this.fadeTimer.target, 0, 1, 0)
			.limit(0, 1)
			* this._volume;

    if (v <= 0.01) {
      this.stop();
      this.currentTrack.volume = this._volume;
      clearInterval(this._fadeInterval);
    }
    else {
      this.currentTrack.volume = v;
    }
  },

  _endedCallback: function () {
    if (this._loop) {
      this.play();
    }
    else {
      this.next();
    }
  }
});



Eg.Sound = Eg.Class.extend({
  path: '',
  volume: 1,
  currentClip: null,
  multiChannel: true,


  init: function (path, multiChannel) {
    this.path = path;
    this.multiChannel = (multiChannel !== false);

    this.load();
  },


  load: function (loadCallback) {
    if (!Eg.Sound.enabled) {
      if (loadCallback) {
        loadCallback(this.path, true);
      }
      return;
    }

    if (Eg.ready) {
      Eg.soundManager.load(this.path, this.multiChannel, loadCallback);
    }
    else {
      Eg.addResource(this);
    }
  },


  play: function () {
    if (!Eg.Sound.enabled) {
      return;
    }

    this.currentClip = Eg.soundManager.get(this.path);
    this.currentClip.volume = Eg.soundManager.volume * this.volume;
    this.currentClip.play();
  },


  stop: function () {
    if (this.currentClip) {
      this.currentClip.pause();
      this.currentClip.currentTime = 0;
    }
  }
});

Eg.Sound.FORMAT = {
  MP3: { ext: 'mp3', mime: 'audio/mpeg' },
  M4A: { ext: 'm4a', mime: 'audio/mp4; codecs=mp4a' },
  OGG: { ext: 'ogg', mime: 'audio/ogg; codecs=vorbis' },
  WEBM: { ext: 'webm', mime: 'audio/webm; codecs=vorbis' },
  CAF: { ext: 'caf', mime: 'audio/x-caf' }
};
Eg.Sound.use = [Eg.Sound.FORMAT.MP3];
Eg.Sound.channels = 4;
Eg.Sound.enabled = true;




/*********************************** CollisionMap.js ****************************/
Eg.CollisionMap = Eg.Map.extend({
  firstSolidTile: 1,
  lastSolidTile: 255,


  init: function (tilesize, data) {
    this.parent(tilesize, data);
  },


  trace: function (x, y, vx, vy, objectWidth, objectHeight) {
    // Set up the trace-result
    var res = {
      collision: { x: false, y: false },
      pos: { x: x, y: y },
      tile: { x: 0, y: 0 }
    };

    // Break the trace down into smaller steps if necessary
    var steps = Math.ceil(Math.max(Math.abs(vx), Math.abs(vy)) / this.tilesize);
    if (steps > 1) {
      var sx = vx / steps;
      var sy = vy / steps;

      for (var i = 0; i < steps && (sx || sy); i++) {
        this._traceStep(res, x, y, sx, sy, objectWidth, objectHeight);

        x = res.pos.x;
        y = res.pos.y;
        if (res.collision.x) { sx = 0; }
        if (res.collision.y) { sy = 0; }
      }
    }

    // Just one step
    else {
      this._traceStep(res, x, y, vx, vy, objectWidth, objectHeight);
    }

    return res;
  },


  _traceStep: function (res, x, y, vx, vy, width, height) {
    res.pos.x += vx;
    res.pos.y += vy;

    // Horizontal collision (walls)
    if (vx) {
      var pxOffsetX = (vx > 0 ? width : 0);
      var tileOffsetX = (vx < 0 ? this.tilesize : 0);
      var firstTileY = Math.floor(y / this.tilesize);
      var lastTileY = Math.ceil((y + height) / this.tilesize);
      var tileX = Math.floor((x + vx + pxOffsetX) / this.tilesize);

      // Still inside this collision map?
      if (
				lastTileY >= 0 && firstTileY < this.height &&
				tileX >= 0 && tileX < this.width
			) {
        for (var tileY = firstTileY; tileY < lastTileY; tileY++) {
          var t = this.data[tileY] && this.data[tileY][tileX];
          if (t >= this.firstSolidTile && t <= this.lastSolidTile) {
            res.collision.x = true;
            res.tile.x = t;
            res.pos.x = tileX * this.tilesize - pxOffsetX + tileOffsetX;
            break;
          }
        }
      }
    }

    // Vertical collision (floor, ceiling)
    if (vy) {
      var pxOffsetY = (vy > 0 ? height : 0);
      var tileOffsetY = (vy < 0 ? this.tilesize : 0);
      var firstTileX = Math.floor(res.pos.x / this.tilesize);
      var lastTileX = Math.ceil((res.pos.x + width) / this.tilesize);
      var tileY = Math.floor((y + vy + pxOffsetY) / this.tilesize);

      // Still inside this collision map?
      if (
				lastTileX >= 0 && firstTileX < this.width &&
				tileY >= 0 && tileY < this.height
			) {
        for (var tileX = firstTileX; tileX < lastTileX; tileX++) {
          var t = this.data[tileY] && this.data[tileY][tileX];
          if (t >= this.firstSolidTile && t <= this.lastSolidTile) {
            res.collision.y = true;
            res.tile.y = t;
            res.pos.y = tileY * this.tilesize - pxOffsetY + tileOffsetY;
            break;
          }
        }
      }
    }

    // res is changed in place, nothing to return
  }
});


// Static Dummy CollisionMap; never collides
Eg.CollisionMap.staticNoCollision = { trace: function (x, y, vx, vy) {
  return {
    collision: { x: false, y: false },
    pos: { x: x + vx, y: y + vy },
    tile: { x: 0, y: 0 }
  };
}
};


/*********************************** BackgroundMap.js ****************************/
Eg.BackgroundMap = Eg.Map.extend({
  tiles: null,
  scroll: { x: 0, y: 0 },
  distance: 1,
  repeat: false,
  tilesetName: '',
  foreground: false,
  enabled: true,

  preRender: false,
  preRenderedChunks: null,
  chunkSize: 512,
  debugChunks: false,


  anims: {},


  init: function (tilesize, data, tileset) {
    this.parent(tilesize, data);
    this.setTileset(tileset);
  },


  setTileset: function (tileset) {
    this.tilesetName = tileset instanceof Eg.Image ? tileset.path : tileset;
    this.tiles = new Eg.Image(this.tilesetName);
    this.tiles.load();
    this.preRenderedChunks = null;
  },


  setScreenPos: function (x, y) {
    this.scroll.x = x / this.distance;
    this.scroll.y = y / this.distance;
  },


  preRenderMapToChunks: function () {
    var totalWidth = this.width * this.tilesize * Eg.sys.scale,
			totalHeight = this.height * this.tilesize * Eg.sys.scale;

    var chunkCols = Math.ceil(totalWidth / this.chunkSize),
			chunkRows = Math.ceil(totalHeight / this.chunkSize);

    this.preRenderedChunks = [];
    for (var y = 0; y < chunkRows; y++) {
      this.preRenderedChunks[y] = [];

      for (var x = 0; x < chunkCols; x++) {


        var chunkWidth = (x == chunkCols - 1)
					? totalWidth - x * this.chunkSize
					: this.chunkSize;

        var chunkHeight = (y == chunkRows - 1)
					? totalHeight - y * this.chunkSize
					: this.chunkSize;

        this.preRenderedChunks[y][x] = this.preRenderChunk(x, y, chunkWidth, chunkHeight);
      }
    }
  },


  preRenderChunk: function (cx, cy, w, h) {
    var tw = w / this.tilesize / Eg.sys.scale + 1;
    th = h / this.tilesize / Eg.sys.scale + 1;

    var nx = (cx * this.chunkSize / Eg.sys.scale) % this.tilesize,
			ny = (cy * this.chunkSize / Eg.sys.scale) % this.tilesize;

    var tx = Math.floor(cx * this.chunkSize / this.tilesize / Eg.sys.scale),
			ty = Math.floor(cy * this.chunkSize / this.tilesize / Eg.sys.scale);


    var chunk = Eg.$new('canvas');
    chunk.width = w;
    chunk.height = h;

    var oldContext = Eg.sys.context;
    Eg.sys.context = chunk.getContext("2d");

    for (var x = 0; x < tw; x++) {
      for (var y = 0; y < th; y++) {
        if (x + tx < this.width && y + ty < this.height) {
          var tile = this.data[y + ty][x + tx];
          if (tile) {
            this.tiles.drawTile(
							x * this.tilesize - nx, y * this.tilesize - ny,
							tile - 1, this.tilesize
						);
          }
        }
      }
    }
    Eg.sys.context = oldContext;

    return chunk;
  },


  draw: function () {
    if (!this.tiles.loaded || !this.enabled) {
      return;
    }

    if (this.preRender) {
      this.drawPreRendered();
    }
    else {
      this.drawTiled();
    }
  },


  drawPreRendered: function () {
    if (!this.preRenderedChunks) {
      this.preRenderMapToChunks();
    }

    var dx = Eg.sys.getDrawPos(this.scroll.x),
			dy = Eg.sys.getDrawPos(this.scroll.y);


    if (this.repeat) {
      dx %= this.width * this.tilesize * Eg.sys.scale;
      dy %= this.height * this.tilesize * Eg.sys.scale;
    }

    var minChunkX = Math.max(Math.floor(dx / this.chunkSize), 0),
			minChunkY = Math.max(Math.floor(dy / this.chunkSize), 0),
			maxChunkX = Math.ceil((dx + Eg.sys.realWidth) / this.chunkSize),
			maxChunkY = Math.ceil((dy + Eg.sys.realHeight) / this.chunkSize),
			maxRealChunkX = this.preRenderedChunks[0].length,
			maxRealChunkY = this.preRenderedChunks.length;


    if (!this.repeat) {
      maxChunkX = Math.min(maxChunkX, maxRealChunkX);
      maxChunkY = Math.min(maxChunkY, maxRealChunkY);
    }


    var nudgeY = 0;
    for (var cy = minChunkY; cy < maxChunkY; cy++) {

      var nudgeX = 0;
      for (var cx = minChunkX; cx < maxChunkX; cx++) {
        var chunk = this.preRenderedChunks[cy % maxRealChunkY][cx % maxRealChunkX];

        var x = -dx + cx * this.chunkSize - nudgeX;
        var y = -dy + cy * this.chunkSize - nudgeY;
        Eg.sys.context.drawImage(chunk, x, y);
        Eg.Image.drawCount++;

        if (this.debugChunks) {
          Eg.sys.context.strokeStyle = '#f0f';
          Eg.sys.context.strokeRect(x, y, this.chunkSize, this.chunkSize);
        }

        // If we repeat in X and this chunks width wasn't the full chunk size
        // and the screen is not already filled, we need to draw anohter chunk
        // AND nudge it to be flush with the last chunk
        if (this.repeat && chunk.width < this.chunkSize && x + chunk.width < Eg.sys.realWidth) {
          nudgeX = this.chunkSize - chunk.width;
          maxChunkX++;
        }
      }

      // Same as above, but for Y
      if (this.repeat && chunk.height < this.chunkSize && y + chunk.height < Eg.sys.realHeight) {
        nudgeY = this.chunkSize - chunk.height;
        maxChunkY++;
      }
    }
  },


  drawTiled: function () {
    var tile = 0,
			anim = null,
			tileOffsetX = (this.scroll.x / this.tilesize).toInt(),
			tileOffsetY = (this.scroll.y / this.tilesize).toInt(),
			pxOffsetX = this.scroll.x % this.tilesize,
			pxOffsetY = this.scroll.y % this.tilesize,
			pxMinX = -pxOffsetX - this.tilesize,
			pxMinY = -pxOffsetY - this.tilesize,
			pxMaxX = Eg.sys.width + this.tilesize - pxOffsetX,
			pxMaxY = Eg.sys.height + this.tilesize - pxOffsetY;


    // FIXME: could be sped up for non-repeated maps: restrict the for loops
    // to the map size instead of to the screen size and skip the 'repeat'
    // checks inside the loop.

    for (var mapY = -1, pxY = pxMinY; pxY < pxMaxY; mapY++, pxY += this.tilesize) {
      var tileY = mapY + tileOffsetY;

      // Repeat Y?
      if (tileY >= this.height || tileY < 0) {
        if (!this.repeat) { continue; }
        tileY = tileY > 0
					? tileY % this.height
					: ((tileY + 1) % this.height) + this.height - 1;
      }

      for (var mapX = -1, pxX = pxMinX; pxX < pxMaxX; mapX++, pxX += this.tilesize) {
        var tileX = mapX + tileOffsetX;

        // Repeat X?
        if (tileX >= this.width || tileX < 0) {
          if (!this.repeat) { continue; }
          tileX = tileX > 0
						? tileX % this.width
						: ((tileX + 1) % this.width) + this.width - 1;
        }

        // Draw!
        if ((tile = this.data[tileY][tileX])) {
          if ((anim = this.anims[tile - 1])) {
            anim.draw(pxX, pxY);
          }
          else {
            this.tiles.drawTile(pxX, pxY, tile - 1, this.tilesize);
          }
        }
      } // end for x
    } // end for y
  }
});


/*********************************** Loader.js ****************************/
Eg.Loader = Eg.Class.extend({
  status: 0,
  done: false,
  _drawStatus: 0,
  unloaded: [],
  loadCallbackBound: null,
  init: function (opts) {
    this.res = Eg.resources;
    this.gameClass = opts.GameClass;
    this.loadCallbackBound = this.loadCallback.bind(this);
    for (var i = 0; i < this.res.length; i++) {
      this.unloaded.push(this.res[i].path);
    }
  },
  addRes: function (res) {
    this.res.push(res);
    this.unloaded.push(res.path);
  },
  load: function () {
    for (var i = 0; i < this.res.length; i++) {
      this.res[i].load(this.loadCallbackBound);
    }
    this._intervalId = setInterval(this.draw.bind(this), 16);
  },
  loadCallback: function (path, status) {
    if (status) {
      this.unloaded.erase(path);
    }
    else {
      throw ('Failed to load resource: ' + path);
    }

    this.status = 1 - (this.unloaded.length / this.res.length);
    if (this.unloaded.length == 0) { // all done?
      this.done = true;
      var self = this;
      setTimeout(function () {
        clearInterval(self._intervalId);
        Eg.sys.setGame(self.gameClass);
      }, 250);
    }
  },
  draw: function () {
    this._drawStatus += (this.status - this._drawStatus) / 5;
    var w = Eg.sys.realWidth * 0.6;
    var h = Eg.sys.realHeight * 0.1;
    var x = Eg.sys.realWidth * 0.5 - w / 2;
    var y = Eg.sys.realHeight * 0.5 - h / 2;

    Eg.sys.context.fillStyle = '#000';
    Eg.sys.context.fillRect(0, 0, Eg.sys.realWidth, Eg.sys.realHeight);

    Eg.sys.context.fillStyle = '#fff';
    Eg.sys.context.fillRect(x, y, w, h);

    Eg.sys.context.fillStyle = '#000';
    Eg.sys.context.fillRect(x + 1, y + 1, w - 2, h - 2);

    Eg.sys.context.fillStyle = '#fff';
    Eg.sys.context.fillRect(x, y, w * this._drawStatus, h);
  }
});


/*********************************** Enyity.js ****************************/
Eg.Entity = Eg.Class.extend({
  id: 0,
  settings: {},

  size: { x: 16, y: 16 },
  offset: { x: 0, y: 0 },

  pos: { x: 0, y: 0 },
  last: { x: 0, y: 0 },
  vel: { x: 0, y: 0 },
  accel: { x: 0, y: 0 },
  friction: { x: 0, y: 0 },
  maxVel: { x: 100, y: 100 },
  zIndex: 0,
  gravityFactor: 1,
  standing: false,
  bounciness: 0,
  minBounceVelocity: 40,

  anims: {},
  animSheet: null,
  currentAnim: null,
  health: 10,

  type: 0, // TYPE.NONE
  checkAgainst: 0, // TYPE.NONE
  collides: 0, // COLLIDES.NEVER

  _killed: false,

  init: function (x, y, settings) {
    this.id = ++Eg.Entity._lastId;
    this.pos.x = x;
    this.pos.y = y;

    Eg.merge(this, settings);
  },

  addAnim: function (name, frameTime, sequence, stop) {
    if (!this.animSheet) {
      throw ('No animSheet to add the animation ' + name + ' to.');
    }
    var a = new Eg.Animation(this.animSheet, frameTime, sequence, stop);
    this.anims[name] = a;
    if (!this.currentAnim) {
      this.currentAnim = a;
    }

    return a;
  },

  update: function () {
    this.last.x = this.pos.x;
    this.last.y = this.pos.y;
    this.vel.y += Eg.game.gravity * Eg.sys.tick * this.gravityFactor;

    this.vel.x = this.getNewVelocity(this.vel.x, this.accel.x, this.friction.x, this.maxVel.x);
    this.vel.y = this.getNewVelocity(this.vel.y, this.accel.y, this.friction.y, this.maxVel.y);

    // movement & collision
    var mx = this.vel.x * Eg.sys.tick;
    var my = this.vel.y * Eg.sys.tick;
    var res = Eg.game.collisionMap.trace(
			this.pos.x, this.pos.y, mx, my, this.size.x, this.size.y
		);
    this.handleMovementTrace(res);

    if (this.currentAnim) {
      this.currentAnim.update();
    }
  },


  getNewVelocity: function (vel, accel, friction, max) {
    if (accel) {
      return (vel + accel * Eg.sys.tick).limit(-max, max);
    }
    else if (friction) {
      var delta = friction * Eg.sys.tick;

      if (vel - delta > 0) {
        return vel - delta;
      }
      else if (vel + delta < 0) {
        return vel + delta;
      }
      else {
        return 0;
      }
    }
    return vel.limit(-max, max);
  },


  handleMovementTrace: function (res) {
    this.standing = false;
    if (res.collision.y) {
      if (this.bounciness > 0 && Math.abs(this.vel.y) > this.minBounceVelocity) {
        this.vel.y *= -this.bounciness;
      }
      else {
        if (this.vel.y > 0) {
          this.standing = true;
        }
        this.vel.y = 0;
      }
    }
    if (res.collision.x) {
      if (this.bounciness > 0 && Math.abs(this.vel.x) > this.minBounceVelocity) {
        this.vel.x *= -this.bounciness;
      }
      else {
        this.vel.x = 0;
      }
    }
    this.pos = res.pos;
  },


  draw: function () {
    if (this.currentAnim) {
      this.currentAnim.draw(
				Math.round(this.pos.x) - this.offset.x - Eg.game.screen.x,
				Math.round(this.pos.y) - this.offset.y - Eg.game.screen.y
			);
    }
  },


  kill: function () {
    Eg.game.removeEntity(this);
  },


  receiveDamage: function (amount, from) {
    this.health -= amount;
    if (this.health <= 0) {
      this.kill();
    }
  },


  touches: function (other) {
    return !(
			this.pos.x > other.pos.x + other.size.x ||
			this.pos.x + this.size.x < other.pos.x ||
			this.pos.y > other.pos.y + other.size.y ||
			this.pos.y + this.size.y < other.pos.y
		);
  },


  distanceTo: function (other) {
    var xd = (this.pos.x + this.size.x / 2) - (other.pos.x + other.size.x / 2);
    var yd = (this.pos.y + this.size.y / 2) - (other.pos.y + other.size.y / 2);
    return Math.sqrt(xd * xd + yd * yd);
  },


  angleTo: function (other) {
    return Math.atan2(
			(other.pos.y + other.size.y / 2) - (this.pos.y + this.size.y / 2),
			(other.pos.x + other.size.x / 2) - (this.pos.x + this.size.x / 2)
		);
  },


  check: function (other) { },
  collideWith: function (other, axis) { }
});


// Last used entity id; incremented with each spawned entity

Eg.Entity._lastId = 0;


// Collision Types - Determine if and how entities collide with each other

// In ACTIVE vs. LITE or FIXED vs. ANY collisions, only the "weak" entity moves,
// while the other one stays fixed. In ACTIVE vs. ACTIVE and ACTIVE vs. PASSIVE
// collisions, both entities are moved. LITE or PASSIVE entities don't collide
// with other LITE or PASSIVE entities at all. The behaiviour for FIXED vs.
// FIXED collisions is undefined.

Eg.Entity.COLLIDES = {
  NEVER: 0,
  LITE: 1,
  PASSIVE: 2,
  ACTIVE: 4,
  FIXED: 8
};


// Entity Types - used for checks

Eg.Entity.TYPE = {
  NONE: 0,
  A: 1,
  B: 2,
  BOTH: 3
};



Eg.Entity.checkPair = function (a, b) {

  // Do these entities want checks?
  if (a.checkAgainst & b.type) {
    a.check(b);
  }

  if (b.checkAgainst & a.type) {
    b.check(a);
  }

  // If this pair allows collision, solve it! At least one entity must
  // collide ACTIVE or FIXED, while the other one must not collide NEVER.
  if (
		a.collides && b.collides &&
		a.collides + b.collides > Eg.Entity.COLLIDES.ACTIVE
	) {
    Eg.Entity.solveCollision(a, b);
  }
};


Eg.Entity.solveCollision = function (a, b) {

  // If one entity is FIXED, or the other entity is LITE, the weak
  // (FIXED/NON-LITE) entity won't move in collision response
  var weak = null;
  if (
		a.collides == Eg.Entity.COLLIDES.LITE ||
		b.collides == Eg.Entity.COLLIDES.FIXED
	) {
    weak = a;
  }
  else if (
		b.collides == Eg.Entity.COLLIDES.LITE ||
		a.collides == Eg.Entity.COLLIDES.FIXED
	) {
    weak = b;
  }


  // Did they already overlap on the X-axis in the last frame? If so,
  // this must be a vertical collision!
  if (
		a.last.x + a.size.x > b.last.x &&
		a.last.x < b.last.x + b.size.x
	) {
    // Which one is on top?
    if (a.last.y < b.last.y) {
      Eg.Entity.seperateOnYAxis(a, b, weak);
    }
    else {
      Eg.Entity.seperateOnYAxis(b, a, weak);
    }
    a.collideWith(b, 'y');
    b.collideWith(a, 'y');
  }

  // Horizontal collision
  else if (
		a.last.y + a.size.y > b.last.y &&
		a.last.y < b.last.y + b.size.y
	) {
    // Which one is on the left?
    if (a.last.x < b.last.x) {
      Eg.Entity.seperateOnXAxis(a, b, weak);
    }
    else {
      Eg.Entity.seperateOnXAxis(b, a, weak);
    }
    a.collideWith(b, 'x');
    b.collideWith(a, 'x');
  }
};


// FIXME: This is a mess. Instead of doing all the movements here, the entities
// should get notified of the collision (with all details) and resolve it
// themselfs.

Eg.Entity.seperateOnXAxis = function (left, right, weak) {
  var nudge = (left.pos.x + left.size.x - right.pos.x);

  // We have a weak entity, so just move this one
  if (weak) {
    var strong = left === weak ? right : left;
    weak.vel.x = -weak.vel.x * weak.bounciness + strong.vel.x;

    var resWeak = Eg.game.collisionMap.trace(
			weak.pos.x, weak.pos.y, weak == left ? -nudge : nudge, 0, weak.size.x, weak.size.y
		);
    weak.pos.x = resWeak.pos.x;
  }

  // Normal collision - both move
  else {
    var v2 = (left.vel.x - right.vel.x) / 2;
    left.vel.x = -v2;
    right.vel.x = v2;

    var resLeft = Eg.game.collisionMap.trace(
			left.pos.x, left.pos.y, -nudge / 2, 0, left.size.x, left.size.y
		);
    left.pos.x = Math.floor(resLeft.pos.x);

    var resRight = Eg.game.collisionMap.trace(
			right.pos.x, right.pos.y, nudge / 2, 0, right.size.x, right.size.y
		);
    right.pos.x = Math.ceil(resRight.pos.x);
  }
};


Eg.Entity.seperateOnYAxis = function (top, bottom, weak) {
  var nudge = (top.pos.y + top.size.y - bottom.pos.y);

  // We have a weak entity, so just move this one
  if (weak) {
    var strong = top === weak ? bottom : top;
    weak.vel.y = -weak.vel.y * weak.bounciness + strong.vel.y;

    // Riding on a platform?
    var nudgeX = 0;
    if (weak == top && Math.abs(weak.vel.y - strong.vel.y) < weak.minBounceVelocity) {
      weak.standing = true;
      nudgeX = strong.vel.x * Eg.sys.tick;
    }

    var resWeak = Eg.game.collisionMap.trace(
			weak.pos.x, weak.pos.y, nudgeX, weak == top ? -nudge : nudge, weak.size.x, weak.size.y
		);
    weak.pos.y = resWeak.pos.y;
    weak.pos.x = resWeak.pos.x;
  }

  // Bottom entity is standing - just bounce the top one
  else if (Eg.game.gravity && (bottom.standing || top.vel.y > 0)) {
    var resTop = Eg.game.collisionMap.trace(
			top.pos.x, top.pos.y, 0, -(top.pos.y + top.size.y - bottom.pos.y), top.size.x, top.size.y
		);
    top.pos.y = resTop.pos.y;

    if (top.bounciness > 0 && top.vel.y > top.minBounceVelocity) {
      top.vel.y *= -top.bounciness;
    }
    else {
      top.standing = true;
      top.vel.y = 0;
    }
  }

  // Normal collision - both move
  else {
    var v2 = (top.vel.y - bottom.vel.y) / 2;
    top.vel.y = -v2;
    bottom.vel.y = v2;

    var nudgeX = bottom.vel.x * Eg.sys.tick;
    var resTop = Eg.game.collisionMap.trace(
			top.pos.x, top.pos.y, nudgeX, -nudge / 2, top.size.x, top.size.y
		);
    top.pos.y = resTop.pos.y;

    var resBottom = Eg.game.collisionMap.trace(
			bottom.pos.x, bottom.pos.y, 0, nudge / 2, bottom.size.x, bottom.size.y
		);
    bottom.pos.y = resBottom.pos.y;
  }
};

/*********************************** Game.js ****************************/
Eg.Game = Eg.Class.extend({
  clearColor: '#000000',
  gravity: 0,
  screen: { x: 0, y: 0 },

  entities: [],

  namedEntities: {},
  collisionMap: Eg.CollisionMap.staticNoCollision,
  backgroundMaps: [],
  backgroundAnims: {},

  cellSize: 64,

  _deferredKill: [],
  _levelToLoad: null,
  _doSortEntities: false,

  loadLevel: function (data) {
    this.screen = { x: 0, y: 0 };

    // Entities
    this.entities = [];
    this.namedEntities = {};
    for (var i = 0; i < data.entities.length; i++) {
      var ent = data.entities[i];
      if (ent.type == "EntityPlayer" || ent.type == 'EntityProjectile'
      || ent.type == 'EntityDebris' || ent.type == 'EntityDelay' || ent.type == 'EntityBall'
      || ent.type == 'EntityTrigger' || ent.type == 'EntityEarthquake' || ent.type == 'EntityBlob')
        this.spawnEntity(ent.name || '', ent.type, ent.x, ent.y, ent.settings);
    }
    this.sortEntities();

    // Map Layer
    this.collisionMap = Eg.CollisionMap.staticNoCollision;
    this.backgroundMaps = [];
    for (var i = 0; i < data.layer.length; i++) {
      var ld = data.layer[i];
      if (ld.name == 'collision') {
        this.collisionMap = new Eg.CollisionMap(ld.tilesize, ld.data);
      }
      else {
        var newMap = new Eg.BackgroundMap(ld.tilesize, ld.data, ld.tilesetName);
        newMap.anims = this.backgroundAnims[ld.tilesetName] || {};
        newMap.repeat = ld.repeat;
        newMap.distance = ld.distance;
        newMap.foreground = !!ld.foreground;
        this.backgroundMaps.push(newMap);
      }
    }
  },

  getEntityByName: function (name) {
    return this.namedEntities[name];
  },


  getEntitiesByType: function (type) {
    var entityClass = typeof (type) === 'string' ? window[type] : type;

    var a = [];
    for (var i = 0; i < this.entities.length; i++) {
      var ent = this.entities[i];
      if (ent instanceof entityClass && !ent._killed) {
        a.push(ent);
      }
    }
    return a;
  },


  spawnEntity: function (name, type, x, y, settings) {
    var entityClass = typeof (type) === 'string' ? window[type] : type;

    if (!entityClass) {
      throw ("Can't spawn entity of type " + type);
    }
    var ent = new (entityClass)(x, y, settings || {});
    this.entities.push(ent);
    if (ent.name) {
      this.namedEntities[ent.name] = ent;
    }
    return ent;
  },


  sortEntities: function () {
    this.entities.sort(function (a, b) { return a.zIndex - b.zIndex; });
  },


  sortEntitiesDeferred: function () {
    this._doSortEntities = true;
  },


  removeEntity: function (ent) {
    // Remove this entity from the named entities
    if (ent.name) {
      delete this.namedEntities[ent.name];
    }

    // We can not remove the entity from the entities[] array in the midst
    // of an update cycle, so remember all killed entities and remove
    // them later.
    // Also make sure this entity doesn't collide anymore and won't get
    // updated or checked
    ent._killed = true;
    ent.checkAgainst = Eg.Entity.TYPE.NONE;
    ent.collides = Eg.Entity.COLLIDES.NEVER;
    this._deferredKill.push(ent);
  },

  run: function () {
    this.update();
    this.draw();
  },

  update: function () {
    // load new level?
    if (this._levelToLoad) {
      this.loadLevel(this._levelToLoad);
      this._levelToLoad = null;
    }

    // sort entities?
    if (this._doSortEntities) {
      this.sortEntities();
      this._doSortEntities = false;
    }

    // update entities
    for (var i = 0; i < this.entities.length; i++) {
      var ent = this.entities[i];
      if (!ent._killed) {
        ent.update();
      }
    }
    this.checkEntities();

    // remove all killed entities
    for (var i = 0; i < this._deferredKill.length; i++) {
      this.entities.erase(this._deferredKill[i]);
    }
    this._deferredKill = [];

    // update background animations
    for (var tileset in this.backgroundAnims) {
      var anims = this.backgroundAnims[tileset];
      for (var a in anims) {
        anims[a].update();
      }
    }
  },
  draw: function () {
    if (this.clearColor) {
      Eg.sys.clear(this.clearColor);
    }

    var mapIndex;
    for (mapIndex = 0; mapIndex < this.backgroundMaps.length; mapIndex++) {
      var map = this.backgroundMaps[mapIndex];
      if (map.foreground) {
        // All foreground layers are drawn after the entities
        break;
      }
      map.setScreenPos(this.screen.x, this.screen.y);
      map.draw();
    }

    for (var i = 0; i < this.entities.length; i++) {
      this.entities[i].draw();
    }

    for (mapIndex; mapIndex < this.backgroundMaps.length; mapIndex++) {
      var map = this.backgroundMaps[mapIndex];
      map.setScreenPos(this.screen.x, this.screen.y);
      map.draw();
    }
  },

  checkEntities: function () {
    // Insert all entities into a spatial hash and check them against any
    // other entity that already resides in the same cell. Entities that are
    // bigger than a single cell, are inserted into each one they intersect
    // with.

    // A list of entities, which the current one was already checked with,
    // is maintained for each entity.

    var hash = {};
    for (var e = 0; e < this.entities.length; e++) {
      var entity = this.entities[e];

      // Skip entities that don't check, don't get checked and don't collide
      if (
				e.type == Eg.Entity.TYPE.NONE &&
				e.checkAgainst == Eg.Entity.TYPE.NONE &&
				e.collides == Eg.Entity.COLLIDES.NEVER
			) {
        continue;
      }

      var checked = {},
				xmin = Math.floor(entity.pos.x / this.cellSize),
				ymin = Math.floor(entity.pos.y / this.cellSize),
				xmax = Math.floor((entity.pos.x + entity.size.x) / this.cellSize) + 1,
				ymax = Math.floor((entity.pos.y + entity.size.y) / this.cellSize) + 1;

      for (var x = xmin; x < xmax; x++) {
        for (var y = ymin; y < ymax; y++) {

          // Current cell is empty - create it and insert!
          if (!hash[x]) {
            hash[x] = {};
            hash[x][y] = [entity];
          }
          else if (!hash[x][y]) {
            hash[x][y] = [entity];
          }

          // Check against each entity in this cell, then insert
          else {
            var cell = hash[x][y];
            for (var c = 0; c < cell.length; c++) {

              // Intersects and wasn't already checkd?
              if (entity.touches(cell[c]) && !checked[cell[c].id]) {
                checked[cell[c].id] = true;
                Eg.Entity.checkPair(entity, cell[c]);
              }
            }
            cell.push(entity);
          }
        } // end for y size
      } // end for x size
    } // end for entities
  }
});